69. 只针对异常的情况下才使用异常

      这段代码有什用,看起来根本不明显,这正是它没有真正被使用的原因(详见 67 条)。事实证明,作为一个要对数组元素进行遍历的实现方式,它的构想是十分拙劣的。当这个循环企图访问数组边界之外的第一个数组元素的时候,使用 try-catch 并且忽略 异常的手段来达到终止无限循环的目的。假定它与数组循环的标准模式是等价的,对于它的标准模式每个 Java 程序员都可以一眼辨认出来:

      那么为什么有人会企图使用基于异常的循环,而不是使用行之有效的模式呢?这是他们误以为可以使用 Java 的错误判断机制来提高程序性能,因为 VM 对每次数组访问都要检查越界情况,所以他们认为正常的循环终止测试被编译器隐藏了,但是在 for-each 中仍然可见,这是多余的并且应当避免。这种想法有三个错误:

      实际上基于异常的模式比标准模式要慢得多。在我本地的机器上,对于一个有 100 个元素的数组进行遍历,标准模式比基于异常的模式快了 2 倍。

      这个例子的教训很简单:顾名思义,异常应该只用于异常的情况下;他们永远不应该用于正常的程序控制流程。 一般的,应该优先使用标准的、容易理解的模式,而不是那些声称可以提供更好性能的、弄巧成拙的方法。即使真的能够改进性能,面对平台的不断改进,这种模型的性能优势也不可能一直保持。然而这种过度聪明的模式带来的微妙 Bug 和维护的痛苦将依旧存在。

      这条原则对于 API 设计也有启发。设计良好的 API 不应该强迫它的客户端为了正常的控制流程而使用异常。如果类中具有「状态相关」(state-dependent)的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该具有一个单独的「状态测试」(state-testing)方法,即表明是否可以调用这个状态相关的方法。比如 Iterator 接口含有状态相关的 next 方法,以及相应的状态测试方法 hasNext。这使得利用传统的 for 循环(以及 for-each 循环,在内部使用了 hasNext 方法)对集合进行迭代的标准模式成为可能。

      如果 Iterator 缺少 hasNext 方法,客户端将被迫改用下面的做法:

      另外一种提供单独状态测试的做法是,如果「状态相关」方法无法执行想要的计算,就可以让它返回一个零长度的 optional 值(详见第 55 条),或者返回一个可被识别的返回值,比如 null。

      对于「状态测试方法」和「optional 返回值或者可识别的返回值」这两种做法,有些指导原则可以帮助你在两者之间做出选择。如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,就必须使用「optional 返回值或者可识别的返回值」,因为在调用「状态测试」方法和调用对应的「状态相关」方法的时间间隔之中,对象的状态有可能发生变化。如果单独的「状态测试」方法必须重复「状态相关」方法的工作,从性能的角度考虑,就必须使用可被识别的返回值。如果其他方面都是等同的,那么「状态测试」方法则优于可被识别的返回值。他提供了相对更高的可读性,对于使用不当的情形可能更加易于检测和改正:如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使得这个 Bug 变得很明显;如果忘了去检查可识别的返回值,这个 Bug 就很难被发现。optional 返回值不会有这方面的问题。

      总而言之,异常是为了在异常情况下被设计和使用的。不要将它们勇于普通的控制流程,也不要编写迫使它们这么做的 API。